package emulator.hardware.nmos6502;

import java.io.PrintStream;
import java.util.Observer;
import emulator.EmulatorException;
import emulator.hardware.CPU;
import emulator.hardware.CPUEventHandler;
import emulator.hardware.CPUExecutionObserver;
import emulator.hardware.HwByte;
import emulator.hardware.HwNumber;
import emulator.hardware.HwWord;
import emulator.hardware.NoExecutionObserver;
import emulator.hardware.bits.EdgeDetector;
import emulator.hardware.bits.SharedBit;
import emulator.hardware.bus.Bus;
import emulator.hardware.clock.Clock;
import emulator.hardware.clock.ClockHandle;
import emulator.hardware.debug.BreakpointException;
import emulator.hardware.debug.BusWatchException;
import emulator.hardware.memory.UnmappedMemoryException;
import emulator.hardware.nmos6502.commands.IllegalOpcodeException;
import emulator.hardware.nmos6502.stack.CpuStack;
import emulator.hardware.nmos6502.stack.StackData;
import emulator.hardware.nmos6502.stack.StackOverflowException;
import emulator.hardware.nmos6502.stack.StackUnderflowException;
import emulator.support.CpuState;
import emulator.util.SynchronizedFlag;

/** The good old 6502 CPU */
public class Cpu6502 extends Thread implements CPU {

	public static final int NMI_VECTOR = 0xFFFA;
	public static final int RESET_VECTOR = 0xFFFC;
	public static final int IRQ_VECTOR = 0xFFFE;
	private static final int NO_INTERRUPT = -1;

	private SharedBit irq_in = new SharedBit();
	private SharedBit nmi_in = new EdgeDetector(false, false);
	private int pc = 0;
	private HwByte accu = new HwByte();
	private HwByte reg_x = new HwByte();
	private HwByte reg_y = new HwByte();
	Flags6502 flags = new Flags6502(); 
	private CpuStack stack = new CpuStack();
	private CommandSet commands = new CommandSet();
	private CPUEventHandler handler;
	private CPUExecutionObserver execution_observer = new NoExecutionObserver();
	private BreakPoints breakpoints = new BreakPoints();
	private Bus bus = null;
	private ClockHandle clock = null;

	private boolean run_cont = false;
	private boolean run_once = false;
	private boolean break_on_return = false;
	
	private boolean internal_inhibit_interrupt = false;
	
	private int interrupt = RESET_VECTOR;
	private SynchronizedFlag is_running = new SynchronizedFlag();
	private int call_depth;
	
	public void addRunningStateObserver(Observer observer)
	{
		is_running.addObserver(observer);
	}

	public void deleteRunningStateObserver(Observer observer)
	{
		is_running.deleteObserver(observer);
	}

	public CpuStack getStack()
	{
		return stack;
	}

	public void attach(Bus bus, Clock clock)
	{
		this.bus = bus;
		if (clock != null)
			this.clock = clock.acquireHandle();
		stack.setBus(bus);
	}
	
	public void detach()
	{
		this.bus = null;
		this.clock = null;
	}
	
	public SharedBit getIrqIn()
	{
		return irq_in;
	}
	
	public SharedBit getNmiIn()
	{
		return nmi_in;
	}
	
	@Override
	public void irq() throws UnmappedMemoryException, EmulatorException {
		interrupt = IRQ_VECTOR;
	}

	@Override
	public void nmi() throws UnmappedMemoryException, EmulatorException {
		interrupt = NMI_VECTOR;
	}
	
	@Override
	public void reset() throws EmulatorException {
		interrupt = RESET_VECTOR;
		stack.setSP(0xFF);
		is_running.touch();
	}

	@Override
	public void printState(PrintStream out) throws EmulatorException {
		out.println("PC   AC X  Y  Flags    SP  Stack"); // NV-BDIZC
		String flagstr = flags.toFlagString();
		out.println(new HwWord(pc) + " " + accu + " " + reg_x + " " + reg_y + " " + flagstr + " " + stack.getSPByte() + ": " + stack.topOfStackToString());
		printNextOpcode(out);
	}

	public void printNextOpcode(PrintStream out)
	{
		int pc_safe = pc;
		try {
			int opcode = readByte(pc);
			CommandDefinition command = commands.getCommand(new HwByte(opcode));
			Operand operand = command.getOperand();
			pc++;
			String data = new HwByte(opcode) + " ";
			for (int i = 0; i < 2; i++)
			{
				if (i < operand.getByteCount())
					data += new HwByte(readByte(pc)) + " ";
				else
					data += "   ";
				pc++;
			}
			pc -= 2;
			try { operand.init(this); } catch (UnmappedMemoryException e) {}
			out.println(data
						+ " "
						+ command.getCommand().getName()
						+ " "
						+ operand.getString());
		} catch (Exception e) {
		}
		finally
		{
			pc = pc_safe;
		}
	}

	@Override
	synchronized public void go() {
		run_cont = true;
		notifyAll();
	}

	@Override
	synchronized public void step() {
		run_once = true;
		notifyAll();
	}
	
	public void next() throws EmulatorException
	{
		int opcode = fetchProgramByte();
		CommandDefinition command = commands.getCommand(new HwByte(opcode));
		command.getOperand().init(this);
		try {
			tick(command.getCycles());
			command.getCommand().execute(this, command.getOperand());
			if (logTrace()) printState(System.out);
		} catch (IllegalOpcodeException e) {
			pc--;
			throw new IllegalOpcodeException(new HwWord(pc), new HwByte(opcode));
		}
	}

	public void tick(int count) {
		if (clock != null)
			clock.tick(count);
	}

	private boolean logTrace() {
		return false;
	}

	@Override
	synchronized public void halt() {
		run_cont = false;
		run_once = false;
	}

	public int fetchProgramByte() throws UnmappedMemoryException, BusWatchException
	{
		return readByte(pc++);
	}

	public int fetchProgramWord() throws UnmappedMemoryException, BusWatchException
	{
		int data = readWord(pc);
		pc += 2;
		return data;
	}

	public int readWord(int address) throws UnmappedMemoryException, BusWatchException {
		int word_lo = bus.read(address);
		int word_hi = bus.read(address+1);
		return word_lo | (word_hi << 8);
	}

	public int readByte(int address) throws UnmappedMemoryException, BusWatchException {
		return bus.read(address);
	}

	public void writeByte(int address, int data) throws BusWatchException, UnmappedMemoryException {
		bus.write(address, data);
	}

	public HwByte getAccu() {
		return new HwByte(accu.getNumber());
	}

	public HwByte getX() {
		return new HwByte(reg_x.getNumber());
	}

	public HwByte getY() {
		return new HwByte(reg_y.getNumber());
	}
	
	public int getPC() {
		return pc;
	}

	public void setAccu(HwByte data) {
		accu.setNumber(data.getNumber());
	}
	
	public void setX(HwByte data) {
		reg_x.setNumber(data.getNumber());
	}
	
	public void setY(HwByte data) {
		reg_y.setNumber(data.getNumber());
	}
	
	public void setPC(int addr) {
		pc = (addr & 0xFFFF);
	}

	/**
	 * @return the flags
	 */
	public Flags6502 getFlags() {
		return new Flags6502(flags.getNumber());
	}

	/**
	 * @param flags the flags to set
	 */
	public void setFlags(Flags6502 flags) {
		this.flags.setNumber(flags.getNumber());
	}

	/**
	 * @return the sp
	 */
	public HwByte getSP() {
		return stack.getSPByte();
	}

	/**
	 * @param sp the sp to set
	 */
	public void setSP(HwByte sp) {
		stack.setSP(sp);
	}

	public void pushAddress(int word, int source) throws BusWatchException, UnmappedMemoryException, StackOverflowException {
		stack.pushAddress(word, source);
	}

	public void pushByte(int data, int type) throws BusWatchException, UnmappedMemoryException, StackOverflowException {
		stack.pushByte(data, type);
	}
	
	public int popAddress() throws UnmappedMemoryException, BusWatchException, StackUnderflowException {
		return stack.popAddress();
	}

	public int popByte() throws UnmappedMemoryException, BusWatchException, StackUnderflowException {
		return stack.popByte();
	}

	public void branch(HwByte disp) throws EmulatorException {
		int displacement = (int) disp.getNumber();
		if (displacement > 127)
			displacement -= 256;
		pc += displacement;
	}

	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run() {
		for (;;)
		{
			is_running.setValue(true);
			try {
				synchronized(this) {
					if (!(run_cont || run_once))
					{
						clearAllStops();
						is_running.setValue(false);
						while (!(run_cont || run_once))
							wait();
					}
					is_running.setValue(true);
					run_once = false;
				}
				execution_observer.notifyPreExecute(this);
				if (!internal_inhibit_interrupt && isInterruptPending())
				{
					doInterrupt();
					internal_inhibit_interrupt = true;
				}
				else
				{
					try {
						next();
					} catch (BusWatchException e) {
						throw e;
					} finally {
						internal_inhibit_interrupt = false;
					}
				}
				execution_observer.notifyPostExecute(this);
				synchronized(this) {
					int address = getPC();
					if (isBreakpoint(address))
						throw new BreakpointException(address);
				}
			}
			catch (InterruptedException e) { }
			catch (Exception e) {
				halt();
				if (handler != null)
					handler.notify(this, e.getMessage());
			}
		}
	}

	public boolean isBreakpoint(int address) {
		return breakpoints.checkBreak(address);
	}

	private boolean isInterruptPending() {
		evaluateInterruptInputs();
		if (interrupt == IRQ_VECTOR && flags.getInterrupt())
			return false;
		return interrupt != NO_INTERRUPT;
	}

	private void evaluateInterruptInputs() {
		if (interrupt != RESET_VECTOR)
		{
			if (nmi_in.getValue() == true)
				interrupt = NMI_VECTOR;
			else if (irq_in.getValue() == false)
				interrupt = IRQ_VECTOR;
			else
				interrupt = NO_INTERRUPT;
		}
	}

	private void doInterrupt() throws BusWatchException,
			UnmappedMemoryException, StackOverflowException
	{
		if (interrupt != RESET_VECTOR)
		{
			pushAddress(pc, StackData.SOURCE_INTERRUPT);
			tick(2);
			pushByte((int) flags.getNumber(), StackData.DATA_FLAGS);
			tick(1);
		}
		flags.setInterrupt(true);
		setPC(readWord(interrupt));
		tick(2);
		interrupt = NO_INTERRUPT;
	}

	@Override
	public void waitForHalt() {
		try {
			while (run_cont || run_once)
				is_running.waitForValue(false);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void setEventHandler(CPUEventHandler handler) {
		this.handler = handler;
	}

	@Override
	synchronized public void breakAt(HwNumber address, boolean enable) {
		if (enable)
			breakpoints.addBreak((int)address.getNumber());
		else
			breakpoints.removeBreak((int)address.getNumber());
	}

	@Override
	public void clearAllStops() {
		breakpoints.clearAllStops();
		enableBreakOnReturn(false);
	}

	@Override
	public void enableBreakOnReturn(boolean enable) {
		break_on_return = enable;
		if (enable)
			call_depth = 1;
	}

	@Override
	public void stopOnceAt(HwNumber address) {
		breakpoints.addStop((int)address.getNumber());
	}

	public void checkBreakOnReturn() throws BreakpointException {
		call_depth--;
		if (break_on_return && call_depth == 0)
			throw new BreakpointException(getPC());
	}
	
	public void notifyCall()
	{
		call_depth++;
	}

	@Override
	public void stepOver() {
		call_depth = 0;
		step();
		waitForHalt();
		if (call_depth > 0)
		{
			enableBreakOnReturn(true);
			go();
		}
	}

	@Override
	public void setValue(String name, int value) {
		String name_lower = name.toLowerCase();
		if (name_lower.equals("ac"))
			accu.setNumber(value);
		else if (name_lower.equals("x"))
			reg_x.setNumber(value);
		else if (name_lower.equals("y"))
			reg_y.setNumber(value);
		else if (name_lower.equals("sp"))
			stack.setSP(value);
		else if (name_lower.equals("pc"))
			setPC(value);
		else if (name_lower.equals("flags"))
			flags.setNumber(value);	
	}

	public boolean isRunning() {
		return is_running.getValue();
	}
	
	public long getTicks()
	{
		if (clock != null)
			return clock.getTicks();
		return 0;
	}

	@Override
	public void setExecutionObserver(CPUExecutionObserver observer) {
		if (observer != null)
			execution_observer = observer;
		else
			execution_observer = new NoExecutionObserver();
	}

	@Override
	public CpuState getCpuState() {
		CpuState cpu_state = new CpuState();
		cpu_state.setValue("A", (int) getAccu().getNumber());
		cpu_state.setValue("X", (int) getX().getNumber());
		cpu_state.setValue("Y", (int) getY().getNumber());
		cpu_state.setValue("SP", (int) getSP().getNumber());
		cpu_state.setValue("PC", getPC());
		cpu_state.setValue("Flags", (int) getFlags().getNumber()); 
		return cpu_state;
	}
}
